package org.gpp.proj1.logic;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.gpp.proj1.communication.CommunicationException;
import org.gpp.proj1.communication.listener.ConnectionAttemptListener;
import org.gpp.proj1.logic.core.client.Client;
import org.gpp.proj1.logic.core.server.Server;
import org.gpp.proj1.logic.core.server.ServerPhaseException;
import org.gpp.proj1.logic.metadata.PartCategory;
import org.gpp.proj1.logic.metadata.PartEntry;
import org.gpp.proj1.logic.metadata.RobotEntry;
import org.gpp.proj1.logic.metadata.RobotPartCategory;
import org.gpp.proj1.logic.profile.PlayerProfile;
import org.gpp.proj1.logic.profile.UnsufficientFundsException;
import org.gpp.proj1.logic.profile.manager.ProfileManager;
import org.gpp.proj1.logic.profile.manager.ProfileManagerException;
import org.gpp.proj1.logic.shop.PartShop;
import org.gpp.proj1.logic.shop.list.PartListLoader;
import org.gpp.proj1.ui.controller.ISRGUIController;

import com.jme.util.LoggingSystem;

/**
 * @author Paolo Victor, paolovictor@gmail.com
 */
public class SRGUIModelImpl implements ISRGUIModel {
	/**
	 * The default parts list file
	 */
	private static final String PART_LIST_FILE = PartListLoader.DEFAULT_PARTS_LIST;

	/**
	 * The current player profile
	 */
	private PlayerProfile currentProfile = null;
	
	/**
	 * The profile manager
	 */
	private ProfileManager profileManager;	
	
	/**
	 * 
	 * The controller assigned to this model. It's used
	 * to notify the view
	 */
	private ISRGUIController controller = null;

	/**
	 * The part shop instance
	 */
	private PartShop partShop;

	/**
	 * The game's parts
	 */
	private Map<PartCategory, Map<String, PartEntry>> parts;

	private Server gameServer;
	
	private Client gameClient;
	
	/**
	 * @param profileFilePath The path of the profiles path
	 */
	public SRGUIModelImpl( String profileFilePath ) {

		try {
			this.profileManager = new ProfileManager( profileFilePath );
		} catch (ProfileManagerException e) {
			LoggingSystem.getLogger().warning( "Can't load profiles at: \"" + profileFilePath + "\"" );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "SRGUIModelImpl", e );
			
			this.profileManager = new ProfileManager();
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#createPlayerProfile(java.lang.String)
	 */
	public void createPlayerProfile(String playerName) {
		
		this.profileManager.addProfile( new PlayerProfile( playerName ) );
		try {
			this.profileManager.saveProfiles();
		} catch (ProfileManagerException e) {
			LoggingSystem.getLogger().warning( "Could not save profiles!" );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "createPlayerProfile", e );
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getPlayerProfileNames()
	 */
	public Set<String> getPlayerProfileNames() {
		
		return profileManager.getPlayerProfileNames();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#setController(org.gpp.proj1.ui.controller.ISRGUIController)
	 */
	public void setController(ISRGUIController controller) {
		
		this.controller = controller;
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#start()
	 */
	public void start() {
		
		if( controller == null ) {
			throw new IllegalStateException( "Can't start the model without a controller" );
		}
		
		this.controller.notifyPromptLoadProfile(); 
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#notifyProfileNotLoaded()
	 */
	public void notifyProfileNotLoaded() {
		
		if( currentProfile == null ) {
			this.controller.notifyAborting( "Profile not loaded. Aborting." );
		}
		
		System.exit( 0 );
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#loadProfile(java.lang.String)
	 */
	public void loadProfile(String playerName) {
		
		this.currentProfile = profileManager.getProfile(playerName);
		
		this.partShop = new PartShop( this.getPartsList(), this.currentProfile );
		
		this.controller.notifyLoadedProfile();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getCurrentProfile()
	 */
	public PlayerProfile getCurrentProfile() {
		
		if( currentProfile != null ) {
			return currentProfile;
		} else {
			return null;
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getPartsList()
	 */
	public Map<PartCategory, Map<String, PartEntry>> getPartsList() {

		if( this.parts == null ) {
			try {
				this.parts = PartListLoader.loadParts( PART_LIST_FILE );
			} catch (FileNotFoundException e) {
				LoggingSystem.getLogger().warning( "Can't load part file: \"" + PART_LIST_FILE + "\"" );
				LoggingSystem.getLogger().throwing( this.getClass().getName(), "loadParts", e );
				
				this.parts = new HashMap<PartCategory, Map<String, PartEntry>>();
			}
		}
		
		return this.parts;
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#buyPart(java.lang.String, java.lang.String, int)
	 */
	public synchronized void buyPart(PartCategory category, String part, int quantity) throws UnsufficientFundsException {

		this.partShop.buyPart( category, part, quantity );
		this.profileManager.updateProfile( currentProfile );
		try {
			this.profileManager.saveProfiles();
		} catch (ProfileManagerException e) {
			LoggingSystem.getLogger().warning( "Can't save profiles file." );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "buyPart", e );
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getAvailableParts(org.gpp.proj1.logic.metadata.PartCategory)
	 */
	public Map<PartEntry, Integer> getAvailableParts(PartCategory category) {
		
		return this.currentProfile.getAvailableParts( category );
	}


	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#saveRobot(java.lang.String, java.lang.String, java.util.Map)
	 */
	public void saveRobot(String oldRobot, String newRobot, Map<RobotPartCategory, PartEntry> parts) {
		
		RobotEntry oldEntry = null;
		RobotEntry newEntry = null;
		
		if( this.currentProfile.existsRobot( oldRobot ) ) {
			oldEntry = this.currentProfile.getRobot( oldRobot );
		}
		
		newEntry = new RobotEntry( this.currentProfile.getName(), newRobot, parts, 0, 0 );
		
		this.currentProfile.saveRobot( oldEntry, newEntry );
		
		try {
			this.profileManager.saveProfiles();
		} catch (ProfileManagerException e) {
			e.printStackTrace();
			
			LoggingSystem.getLogger().warning( "Can't save profiles file." );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "buyPart", e );
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getPlayerRobots()
	 */
	public List<RobotEntry> getPlayerRobots() {
		
		return this.currentProfile.getPlayerRobots();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getRobotDescription(java.lang.String)
	 */
	public String getRobotDescription(String robot) {

		return this.currentProfile.getRobot( robot ).getDescription();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#deleteRobot(java.lang.String)
	 */
	public void deleteRobot(String robotName) {

		this.currentProfile.deleteRobot( robotName );
		
		try {
			this.profileManager.saveProfiles();
		} catch (ProfileManagerException e) {
			LoggingSystem.getLogger().warning( "Can't save profiles file." );
			LoggingSystem.getLogger().throwing( this.getClass().getName(), "buyPart", e );
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#startServer(java.lang.String, int, java.lang.String, java.io.File, int)
	 */
	public void startServer(String name, int maxPlayers, String mapName, File mapFile, int port) throws CommunicationException {
		
		if( this.isClientConnected() ) {
			throw new CommunicationException("Already connected");
		}
		
		this.gameServer = new Server( name, maxPlayers, mapName, mapFile, port );
		this.gameServer.start();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#isClientConnected()
	 */
	public boolean isClientConnected() {
		return this.gameClient != null && this.gameClient.isConnected();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#closeConnection()
	 */
	public void closeConnection() throws CommunicationException {
		
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}

		if( this.gameClient.isConnected() ) {
			this.gameClient.disconnect();
			this.gameClient = null;
		}
		
		if( this.gameServer != null ) {
			this.gameServer.stop();
			this.gameServer = null;
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#connectToServer(org.gpp.proj1.communication.listener.ConnectionAttemptListener, java.lang.String, int)
	 */
	public void connectToServer(ConnectionAttemptListener listener, String address, int localPort) throws CommunicationException {
		
		this.gameClient = new Client( this.controller, this.getCurrentProfile().getName(), localPort );
		this.gameClient.setConnectionAttemptListener( listener );
		
		StringTokenizer tk = new StringTokenizer( address, ":" );
		
		try {
			this.gameClient.connect( tk.nextToken(), Integer.parseInt( tk.nextToken() ) );
		} catch (NumberFormatException e) {
			throw new CommunicationException( "Invalid port", e );
		}
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#sendChatMessage(java.lang.String)
	 */
	public void sendChatMessage(String text) throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		this.gameClient.sendChat( text );
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#changeGameMap(java.lang.String)
	 */
	public void changeGameMap(String mapName) throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		if( ! this.isServer() ) {
			throw new CommunicationException("Not a server");
		}
		
		this.gameServer.changeMap( mapName );
	}

	/**
	 * @return If the client is a game server
	 */
	public boolean isServer() {

		return this.gameServer != null;
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#toggleReadinessState()
	 */
	public void toggleReadinessState() throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		this.gameClient.toggleReadinessState();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#mayStartGame()
	 */
	public boolean mayStartGame() {

		return this.isClientConnected() && this.isServer() && this.gameServer.mayInitGame();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#disconnect()
	 */
	public void disconnect() throws CommunicationException {
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		this.gameClient.disconnect();
		
		if( this.isServer() ) {
			this.gameServer.stop();
		}
		
		this.controller.notifyDisconnected();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#kickPlayer(java.lang.String)
	 */
	public void kickPlayer(String clientId) throws CommunicationException {
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		if( ! this.isServer() ) {
			throw new CommunicationException("Not a server");
		}
		
		this.gameServer.kickClient( clientId );
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#initGame()
	 */
	public void initGame() throws CommunicationException, ServerPhaseException {
		
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		if( ! this.isServer() ) {
			throw new CommunicationException("Not a server");
		}
		
		this.gameServer.initGame();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#notifyReadyToStartGame()
	 */
	public void notifyReadyToStartGame() throws CommunicationException {
		
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		this.gameClient.notifyReadyToStartGame();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#getCurrentPlayerId()
	 */
	public String getCurrentPlayerId() throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}
		
		return this.gameClient.getCurrentPlayerId();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#endTurn()
	 */
	public void endTurn() throws CommunicationException {
		
		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}

		this.gameClient.notifyEndTurn();
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#movedRobot(java.lang.String, int, int)
	 */
	public void movedRobot(String robot, int newX, int newY) throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}

		this.gameClient.movedRobot( robot, newX, newY );
	}

	/* (non-Javadoc)
	 * @see org.gpp.proj1.logic.ISRGUIModel#robotAttacked(java.lang.String, int, int, java.lang.String, java.lang.String)
	 */
	public void robotAttacked(String robot, int selectedX, int selectedY, String selectedPart, String selectedAttack) throws CommunicationException {

		if( ! this.isClientConnected() ) {
			throw new CommunicationException("Not connected");
		}

		this.gameClient.robotAttacked(robot, selectedX, selectedY, selectedPart, selectedAttack);
	}
}
